import sys
from time import sleep # sleep用来暂停时间
import pygame

from ship import Ship
from bullet import Bullet
from alien import Alien
from game_stats import GameStats
from button import Button
from scoreboard import ScoreBoard

from settings import Settings

class AlienInvasion:
    def __init__(self) -> None:
        pygame.init() # 初始化背景设置
        self.settings = Settings() # 设置一个专门的类来初始化各种设置，管理各种设置

        self.screen = pygame.display.set_mode((
            self.settings.screen_width, self.settings.screen_height)) # 创建显示窗口，尺寸为1200*800，display.set_mode()返回一个surface对象，激活游戏内的动画循环后，每次循环都会重绘此对象

        # self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN) # 设置全屏，坐标是(0,0)，
        # self.settings.screen_width = self.screen.get_rect().width # 由于预先不知道全屏的尺寸，现在获取，并在以后刷新时设置为这个大小
        # self.settings.screen_height = self.screen.get_rect().height

        pygame.display.set_caption("ALien Invasion") # 设置标题

        # 创建用于存储游戏统计信息的实例
        self.stats = GameStats(self)

        # 初始化飞船对象
        self.ship = Ship(self)

        # 子弹编组
        self.bullets = pygame.sprite.Group() # 先创建group对象，再向编组内添加bullet对象

        # 外星人编组
        self.aliens = pygame.sprite.Group()
        self._create_fleet() # 初始化时就已添加alien对象在里面

        # 创建Play按钮
        self.play_button = Button(self, "Play")

        # 创建得分显示图像（计分板）
        self.sb = ScoreBoard(self)

    def run_game(self):
        while True:
            self._chek_event() # 调用检查输入的辅助方法
            if self.stats.game_active: # 如果游戏处于活动状态，则运行以下部分
                self.ship.update() # 每次循环刷新前都检查一下移动标志（按键产生的左右移动标志）是否生效，如果生效，就向移动标志的方向移动
                self._update_bullet() # 循环对子弹的检查
                self._update_aliens() # 循环更新aliens

            # print(len(self.bullets))

            self._update_screen() # 调用刷新屏幕辅助方法，此处只是把旧屏幕替换成新屏幕

    def _chek_event(self): # 辅助方法：检查发生事件。 辅助方法以_打头
        for event in pygame.event.get(): # event.get()返回一个列表，包含从上次该函数被调用后直到此次被调用所发生的所有事件
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN: # keydown按下 keyup弹起
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)
            elif event.type == pygame.MOUSEBUTTONDOWN:
                mouse_pos = pygame.mouse.get_pos() # 假如鼠标按下，检查当前鼠标的位置
                self._check_play_button(mouse_pos)

    def _check_keydown_events(self, event):
        # 响应按下键
        if event.key == pygame.K_RIGHT:
            # 向右移动
            # self.ship.rect.x += 1 # 每次按右方向键，ship的位置rect对象x坐标加1
            self.ship.moving_right = True # 按下时，将移动标志置为True
        elif event.key == pygame.K_LEFT: # 此处可用elif是因为event只会是一种值
            self.ship.moving_left = True
        elif event.key == pygame.K_q: # K_q：q键，设置为退出
            sys.exit()
        elif event.key == pygame.K_SPACE: # 响应空格键开火
            self._fire_bullet()

    def _check_keyup_events(self, event):
        # 响应松开键
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False # 松开时，将移动标志置为False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _check_play_button(self, mouse_pos):
        """在玩家单击Play时开始新游戏"""
        button_clicked = self.play_button.rect.collidepoint(mouse_pos)
        if button_clicked and not self.stats.game_active: # 使用碰撞检测确认是否位置符合，且仅当游戏未处于活动状态时才能响应单击开始
            # 重置游戏统计信息
            self.stats.reset_status() # 点击开始时，重置剩余飞船数和得分
            self.stats.game_active = True
            self.sb.prep_score() # 重新显示得分
            self.sb.prep_level() # 每次点击开始，重置level的显示
            self.sb.prep_ships() # 重置ships的显示数量
            # 重置游戏设置
            self.settings.initialize_dynamic_settings() # 单击开始时，重置游戏难度
            # 清空剩余的alien和bullet
            self.aliens.empty()
            self.bullets.empty()
            # 创建一群新的外星人,并将飞船复位
            self._create_fleet()
            self.ship.center_ship()
            # 隐藏鼠标光标
            pygame.mouse.set_visible(False)
        
    def _update_screen(self): # 辅助方法：更新屏幕
        # 设置屏幕
        self.screen.fill(self.settings.bg_color) # fill()屏幕填充输入的颜色
        self.ship.blitme() # 绘制飞船
        for bullet in self.bullets.sprites(): # 逐个绘制子弹
            bullet.draw_bullet()
        self.aliens.draw(self.screen) # 绘制外星人
        self.sb.show_score() # 绘制计分板

        # 如果游戏处于非活动状态，就绘制Play按钮
        if not self.stats.game_active:
            self.play_button.draw_button()

        # 刷新屏幕，即切换到新屏幕
        pygame.display.flip() # 刷新屏幕，只是让最近绘制的屏幕可见，不包含设置屏幕

    def _fire_bullet(self):
        # 按下开火键后，生成一个子弹对象
        if len(self.bullets) < self.settings.bullet_allowed: # 限制最大子弹数
            new_bullet = Bullet(self)
            self.bullets.add(new_bullet) # 往group内添加一个bullet对象

    def _update_bullet(self):
        self.bullets.update() # 对编组调用update()时，相当于对编组内每个精灵(sprite)调用一个update()，每次循环都会先更新子弹位置
        for bullet in self.bullets.sprites(): # 检查是否需要删除子弹（在碰到屏幕顶部之后）
            if bullet.rect.y <= 0:
                self.bullets.remove(bullet)

        self._check_bullet_alien_collisions()

    def _check_bullet_alien_collisions(self):
        # 碰撞检测，是否击中外星人
        collisons = pygame.sprite.groupcollide(self.bullets, self.aliens, True, True) # 检测两个group内的碰撞，两个True表示删除碰撞的两个对象(和前面的一一对应)

        # 击中后，得分
        if collisons: # collisions是一个二维bool列表？
            for aliens in collisons.values(): # 对每一颗子弹都求射中了多少个外星人
                self.stats.score += self.settings.alien_points * len(aliens)
                self.sb.prep_score() # 更新分数
                self.sb.check_high_score() # 每当当前分数增加时都检测一下是否打破最高分纪录

        # 如果没有alien了，就新建一队alien
        if not self.aliens:
            self.bullets.empty() # 同时删除现存的子弹
            self._create_fleet()
            self.settings.increase_speed() # 清除一队alien后，提高游戏难度
            # 提高游戏等级
            self.stats.level += 1
            self.sb.prep_level()

    def _create_fleet(self): # 生成外星舰队   fleet：舰队
        """创建一个alien用来计算一行可容纳alien的数量"""
        alien = Alien(self)
        alien_width, alien_height = alien.rect.size
        # 计算一行可容纳的alien数量
        available_space_x = self.settings.screen_width - (2 * alien_width) # 一行内可放alien的宽度（刨去左侧右侧留的空白，空白宽为alien_width）
        number_alien_x = available_space_x // (2 * alien_width) # 可供一行容纳的alien数量（一个alien计算一个自身宽度和一个离旁边alien的空白宽度alien_width）
        # 计算屏幕可容纳的alien行数
        ship_height = self.ship.rect.height
        available_space_y = (self.settings.screen_height - (3 * alien_height) - ship_height) # 预留3倍alien高度和1倍ship高度的空间，给玩家射杀外星人的时间
        number_rows = available_space_y // (2 * alien_height)
        
        # self.aliens.add(alien)
        for row_number in range(number_rows):
            for alien_number in range(number_alien_x):
                # 创建一行外星人
                self._create_alien(alien_number, row_number)

    def _create_alien(self, alien_number, row_number):
        """创建一个alien"""
        alien = Alien(self)
        alien_width, alien_height = alien.rect.size
        alien.x = alien_width + 2 * alien_width * alien_number
        alien.rect.x = alien.x
        alien.rect.y = alien_height + 2 * alien_height * row_number # 行与行间距为2倍alien高度
        self.aliens.add(alien)

    def _update_aliens(self):
        self._check_fleet_edges() # 检查是否需要转向并向下移动一步
        self.aliens.update() # 对group内所有alien调用update()

        # 检测alien与ship的碰撞
        if pygame.sprite.spritecollideany(self.ship, self.aliens):
            # print("Ship hit!") # 测试代码
            self._ship_hit()
        
        # 检测是否有alien到达屏幕底部
        self._check_aliens_bottom()

    def _check_fleet_edges(self):
        # 有外星人到达边界时转向，只要有一个碰到边界就算碰到，因为同一列的alien是相同的x坐标
        for alien in self.aliens.sprites():
            if alien.check_edges(): # 只要有一个碰撞，就把所有的alien转换方向
                self._change_fleet_direction()
                break

    def _change_fleet_direction(self):
        for alien in self.aliens.sprites(): # 所有的alien均向下移动，且只有碰到边界时才向下移动10px
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1 # 改变左右移动方向
    
    def _ship_hit(self):
        """响应ship与alien碰撞，或者碰到底部也按此处理"""
        if self.stats.ships_left > 0:
            self.stats.ships_left -= 1
            self.sb.prep_ships() # 飞船损失时更新一下
            # 清空余下的alien和bullet
            self.aliens.empty()
            self.bullets.empty()
            # 创建新的alien，并将ship位置复原到底部中间
            self._create_fleet()
            self.ship.center_ship()
            # 暂停一会，让玩家准备
            sleep(0.5) # 0.5s
        else: # 游戏结束
            self.stats.game_active = False # 飞船剩余小于等于0，将游戏设置成结束
            pygame.mouse.set_visible(True) # 游戏结束后，将光标可见
    
    def _check_aliens_bottom(self):
        """检测是否有外星人到底屏幕底部"""
        screen_rect = self.screen.get_rect()
        for alien in self.aliens.sprites():
            if alien.rect.bottom >= screen_rect.bottom: # 只要检测到一个alien到达屏幕底部，就按碰到ship一样处理
                self._ship_hit()
                break

if __name__ == '__main__': # 主函数才会调用，即这个模组被别处引用时不会运行以下的代码
    ai = AlienInvasion()
    ai.run_game()